import org.newdawn.slick.*;
import org.newdawn.slick.geom.Vector2f;
import java.util.ArrayList;

/**
 * This is the base of any object that acts within the game, such as the player, enemies, walls, etc.
 * Contains overridable functions that these game objects can use.
 * 
 * @author Gerrit Jamerson, CodeMonkeys
 * @version 4/3/13
 */
public abstract class GameObject
{
    private String className;
    private ArrayList<GameObject> ignoreObjectList;
    private ArrayList<String> ignoreClassList;

    private Vector2f vecPosition;
    private Vector2f vecVelocity;
    private boolean isCollidable;
    private float fltRotation;

    private BoundBox bounds;

    /**
     * Default constructor
     */
    public GameObject(String clName)
    {
        ignoreObjectList = new ArrayList<GameObject>();
        ignoreClassList = new ArrayList<String>();

        isCollidable = false;

        vecPosition = new Vector2f(0,0);
        vecVelocity = new Vector2f(0,0);

        fltRotation = 0f;
        IGamemode gameMode = GameController.getGameMode();
        ILevelController level = gameMode.getLevelController();

        className = clName;
        level.instantiate(this);
    } //End constructor

    /**
     * Returns the name of the class (passed by super())
     */
    public String getClassName()
    {
        return className;
    }

    /**
     * Removes the gameObject from the game.
     */
    public void destroy()
    {
        GameController.getGameMode().getLevelController().destroy(this);
    } //End destroy method

    /**
     * Sets the position, given two float coordinates.
     * @pre None
     * @param _x The X position.
     * @param _y The Y position.
     * @return void
     * @post The position of the gameObject has been stored.
     */
    public void setPosition(float _x, float _y)
    {
        vecPosition = new Vector2f(_x, _y);
    } //End setPosition method

    /**
     * Sets the position, given two float coordinates.
     * @pre None
     * @param _vecPos The Vector2f position.
     * @return void
     * @post The position of the gameObject has been stored.
     */
    public void setPosition(Vector2f _vecPos)
    {
        vecPosition = new Vector2f(_vecPos);
    } //End setPosition method 2

    /**
     * Sets the velocity, given two float coordinates.
     * @pre None
     * @param _x The X velocity.
     * @param _y The Y velocity.
     * @return void
     * @post The velocity of the gameObject has been stored.
     */
    public void setVelocity(float _x, float _y)
    {
        vecVelocity = new Vector2f(_x, _y);
    } //End setVelocity method

    /**
     * Sets the velocity, given two float coordinates.
     * @pre None
     * @param _vecPos The Vector2f velocity.
     * @return void
     * @post The velocity of the gameObject has been stored.
     */
    public void setVelocity(Vector2f _vecPos)
    {
        vecVelocity = new Vector2f(_vecPos);
    } //End setVelocity method 2

    /**
     * Sets the rotation of the gameObject
     * @pre None
     * @param _fltTheta The angle to change to.
     * @return void
     * @post The rotation of the gameObject has been stored.
     */
    public void setRotation(float _fltTheta)
    {
        fltRotation = _fltTheta;
    } //End setRotation method

    /**
     * Gets the position.
     * @pre None
     * @param None
     * @return The Vector2f position
     * @post None
     */
    public Vector2f getPosition()
    {
        return new Vector2f(vecPosition);
    } //End getPosition method    

    /**
     * Gets the velocity.
     * @pre None
     * @param None
     * @return The Vector2f velocity
     * @post None
     */
    public Vector2f getVelocity()
    {
        return new Vector2f(vecVelocity);
    } //End getVelocity method

    /**
     * Gets the rotation.
     * @pre None
     * @param None
     * @return The float rotation
     * @post None
     */
    public float getRotation()
    {
        return fltRotation;
    } //End getRotation method

    /**
     * Enables collisions with other collidables.
     * @pre None
     * @param bounds The bounding box of this object
     * @return void
     * @post This object now collides with other objects
     */
    public void enableCollisions(BoundBox _bounds)
    {
        isCollidable = true;
        GameController.getGameMode().getLevelController().addCollidable(this);

        bounds = _bounds;
    }

    /**
     * Returns the bounding box
     * @pre Collisions must have been enabled.
     * @param None
     * @return The bounding box of the object
     * @post None
     */
    public BoundBox getBounds()
    {
        return bounds;
    }

    /**
     * Sets the bounding box of the object.
     * @pre Collisions must have been enabled for this to have an effect.
     * @param _bounds The bounding box
     * @return void
     * @post The bounds have been changed.
     */
    public void setBounds(BoundBox _bounds)
    {
        bounds = _bounds;
    }

    /**
     * Disables collisions with other collidables.
     * @pre None
     * @
     */
    public void disableCollisions()
    {
        isCollidable = false;
    }

    /**
     * Called when the object has collided with something.
     * 
     * @pre None
     * @param collision The collision data associated with the collision.
     * @return void
     * @post None
     */
    public void onCollision(CollisionData collision)
    {

    } //End onCollision method

    /**
     * Ignores collision with the given game object.
     * 
     * @pre None
     * @param gameObject The object to ignore collisions with.
     * @return void
     * @post Collisions no longer occur with the game object.
     */
    public void disableObjectCollision(GameObject gameObject)
    {
        if(!ignoreObjectList.contains(gameObject))
        {
            ignoreObjectList.add(gameObject);
        } //End if
    } //End disableObjectCollision method

    /**
     * Enables collision with the given game object.
     * 
     * @pre None
     * @param gameObject The object to enable collisions with.
     * @return void
     * @post Collisions now occur with the game object.
     */
    public void enableObjectCollision(GameObject gameObject)
    {
        ignoreObjectList.remove(gameObject);
    } //End enableObjectCollision method

    /**
     * Returns the list of GameObjects whose collisions should be ignored.
     */
    public ArrayList<GameObject> getIgnoreObjectList()
    {
        return ignoreObjectList;
    } //End getIgnoreObjectList method

    /**
     * Disables collision with a given GameObject class type.
     * 
     * @pre None
     * @param type The class type to ignore collisions with.
     * @return void
     * @post Collisions no longer occur with the class type.
     */
    public void disableClassCollision(String type)
    {
        if(!ignoreClassList.contains(type))
        {
            ignoreClassList.add(type);
        } //End if
    } //End disableClassCollision method

    /**
     * Enables collision with a given GameObject class type, only if it was disabled previously.
     * 
     * @pre None
     * @param type The class type to enable collisions with.
     * @return void
     * @post Collisions now occur with the class type.
     */
    public void enableClassCollision(String type)
    {
        ignoreClassList.remove(type);
    } //End enableClassCollision method

    /**
     * Returns the list containing the classes that should ignore collision.
     */
    public ArrayList<String> getIgnoreClassList()
    {
        return ignoreClassList;
    } //End getIgnoreClassList method

    /**
     * Returns the distance from this game object to a given point.
     * 
     * @pre None
     * @param pos The position to get the distance to.
     * @return The distance
     * @post None
     */
    public float getDistance(Vector2f pos)
    {
        return getPosition().distance(pos);
    } //End getDistance method

    /**
     * Returns the distance from this game object to another.
     * 
     * @pre None
     * @param gameObject The gameObject to get the distance to.
     * @return The distance
     * @post None
     */
    public float getDistance(GameObject gameObject)
    {
        Vector2f pos = gameObject.getPosition();
        return getPosition().distance(pos);
    } //End getDistance method

    /**
     * Returns the angle (theta) from the current position to the given one.
     * 
     * @pre None
     * @param pos The position to get the angle to.
     * @return The angle
     * @post None
     */
    public double getAngle(Vector2f pos)
    {
        Vector2f curPos = getPosition();
        Vector2f temp = new Vector2f(pos.x - curPos.x, pos.y - curPos.y);

        return temp.getTheta();
    } //End getAngle method

    /**
     * Returns the angle (theta) from the current position to the position of the given game object..
     * 
     * @pre None
     * @param gameObject The object to get the angle to.
     * @return The angle
     * @post None
     */
    public double getAngle(GameObject gameObject)
    {
        Vector2f curPos = getPosition();
        Vector2f pos = gameObject.getPosition();
        Vector2f temp = new Vector2f(pos.x - curPos.x, pos.y - curPos.y);

        return temp.getTheta();
    } //End getAngle method

    /**
     * Updates the game object.
     * Only override this when you don't want the default stuff like velocity.
     * 
     * @pre None
     * @param gameContainer the Game Container
     * @param delta The time in milliseconds between updates
     * @return void
     * @post None
     */
    public void update(GameContainer gameContainer, int delta)
    {
        Vector2f vel = new Vector2f(getVelocity().x * delta, getVelocity().y * delta);
        setPosition(getPosition().add(vel));
    } //End update method

    /**
     * Returns true of the boxes overlap
     */
    public boolean collidesWith(GameObject other)
    {
        Vector2f pos = other.getPosition();
        BoundBox b = other.getBounds();

        if(pointInBounds(new Vector2f(pos.x + b.getX(), pos.y + b.getY()))) return true;
        if(pointInBounds(new Vector2f(pos.x + b.getX() + b.getWidth(), pos.y + b.getY()))) return true;
        if(pointInBounds(new Vector2f(pos.x + b.getX(), pos.y + b.getY() + b.getHeight()))) return true;
        if(pointInBounds(new Vector2f(pos.x + b.getX() + b.getWidth(), pos.y + b.getY() + b.getHeight()))) return true;

        return false;
    } //End collides method

    /**
     * Returns true of the boxes overlap
     */
    public boolean collidesWith(GameObject other, Vector2f offset)
    {
        Vector2f pos = other.getPosition();
        BoundBox b = other.getBounds();

        if(pointInBounds(new Vector2f(pos.x + b.getX(), pos.y + b.getY()), offset)) return true;
        if(pointInBounds(new Vector2f(pos.x + b.getX() + b.getWidth(), pos.y + b.getY()), offset)) return true;
        if(pointInBounds(new Vector2f(pos.x + b.getX(), pos.y + b.getY() + b.getHeight()), offset)) return true;
        if(pointInBounds(new Vector2f(pos.x + b.getX() + b.getWidth(), pos.y + b.getY() + b.getHeight()), offset)) return true;

        return false;
    } //End collides method

    /**
     * Returns true if the point is within the box
     */
    private boolean pointInBounds(Vector2f pos)
    {
        if(pos.x > vecPosition.x + bounds.getX() && pos.y > vecPosition.y + bounds.getY())
        {
            if(pos.x < vecPosition.x + bounds.getX() + bounds.getWidth() && pos.y < vecPosition.y + bounds.getY() + bounds.getHeight())
            {
                return true;
            } //End if
        } //End if
        return false;
    } //End pointWithin method

    /**
     * Returns true if the point is within the box
     */
    private boolean pointInBounds(Vector2f pos, Vector2f offset)
    {
        if(pos.x > offset.x + bounds.getX() && pos.y > offset.y + bounds.getY())
        {
            if(pos.x < offset.x + bounds.getX() + bounds.getWidth() && pos.y < offset.y + bounds.getY() + bounds.getHeight())
            {
                return true;
            } //End if
        } //End if
        return false;
    } //End pointWithin method

    /**
     * Checks if this game object would collide with another of a specific type
     */
    public boolean checkCollidesWith(String type, Vector2f pos)
    {
        return GameController.getGameMode().getLevelController().checkCollidesWith(this, type, pos);
    } //End collidesWith method
    
    public abstract void spStep(GameContainer gameContainer, int intDelta);          //Called every step in a singleplayer gamemode.
    public abstract void mpClientStep(GameContainer gameContainer, int intDelta);    //Called every step on the client in a multiplayer gamemode.
    public abstract void mpServerStep(GameContainer gameContainer, int intDelta);    //Called every step on the server in a multiplayer gamemode.
    public abstract void render(GameContainer gameContainer, Graphics graphics);       //Called every frame, used to render the gameObject.
}
